﻿using System;
using System.Collections.Generic;
using BobcatUtility;
using System.IO;
using System.Xml;
using System.Reflection;

namespace BobcatMultiTouch
{
    /// <summary>
    /// Manages a collection of input sources which implement the IMultiTouchSource
    /// interface and raises events (via the same interface) when source data changes.
    /// </summary><remarks>
    /// The purpose of this class is to be able to read input "touch events" from
    /// a number of different underlying source types and make them available in a
    /// common format.
    /// 
    /// Underlying sources must implement the IMultiTouchSource interface and raise
    /// the events specified by the interface. Example sources might be the mouse;
    /// graphics tablets; multi-touch screens; wiimote IR and so on.
    /// 
    /// You can register one or more input sources which implement IMultiTouchSource
    /// with the MultiTouchInputManager (MTIM). You specify which source or sources 
    /// you want to be active at any time and the MTIM will listen to touch events
    /// from them and make them available to its own clients.
    /// 
    /// Singleton
    /// ---------
    /// This class is a singleton. So you need to call the getInstance() method to access
    /// it rather than creating an object of this type.
    /// 
    /// Note about point-indexes
    /// ------------------------
    /// When touch events are fired by this class they include a point-index identifying
    /// which point was moved. We want to keep these indexes consistent across multiple
    /// events. Each source will have its own indexes for the points it manages (which 
    /// must be zero-based). As there may be more than one active source at any one time
    /// this could lead to a conflict between the point-indexes from the different sources.
    /// To avoid this conflict, each source is given its own range of point-indexes. 
    /// 
    /// XML Config
    /// ----------
    /// </remarks>
    public class MultiTouchInputManager
    {
        #region Events

        /// <summary>
        /// Event raised when a touch down event occurs on one of the sources managed by
        /// this object.
        /// </summary>
        public event EventHandler<TouchArgs> TouchDownEvent;

        /// <summary>
        /// Event raised when a touch up event occurs on one of the sources managed by
        /// this object.
        /// </summary>
        public event EventHandler<TouchArgs> TouchUpEvent;

        /// <summary>
        /// Event raised when a mopve event occurs for one of the sources managed by
        /// this object.
        /// </summary>
        public event EventHandler<TouchArgs> TouchMoveEvent;

        #endregion

        #region Public types

        /// <summary>
        /// Holds information about a source, including the source object itself,
        /// whether it is active etc.
        /// </summary>
        public class SourceInfo
        {
            public IMultiTouchSource Source { get; set; }
            public bool Active { get; set; }
            public string SourceXMLConfig { get; set; }
            public int FirstPointIndex { get; set; }
        }

        #endregion

        #region Public methods

        /// <summary>
        /// Returns the instance of this singleton class.
        /// </summary>
        public static MultiTouchInputManager getInstance()
        {
            if (m_Instance == null)
            {
                m_Instance = new MultiTouchInputManager();
            }
            return m_Instance;
        }

        /// <summary>
        /// Called to close down the MTIM, stop listen to events etc.
        /// </summary>
        public void close()
        {
            // We clear the events...
            TouchDownEvent = null;
            TouchUpEvent = null;
            TouchMoveEvent = null;

            // And we stop listening to events from our active sources...
            foreach (KeyValuePair<string, SourceInfo> pair in m_mapInputSources)
            {
                // We find the source, close it and unregister from its events...
                SourceInfo si = pair.Value;
                IMultiTouchSource source = si.Source;
                unregisterEvents(si);
                source.close();
            }

            m_mapInputSources.Clear();
            m_mapFirstPointIndexes.Clear();
        }

        /// <summary>
        /// Returns a collection of source-info objects for the sources we're managing.
        /// </summary>
        public IList<SourceInfo> getSourceInfos()
        {
            IList<SourceInfo> results = new List<SourceInfo>();
            foreach (KeyValuePair<string, SourceInfo> pair in m_mapInputSources)
            {
                results.Add(pair.Value);
            }
            return results;
        }

        /// <summary>
        /// Returns the source-info for the ID passed in. Returns null if the ID isn't found.
        /// </summary>
        public SourceInfo getSourceInfo(string strID)
        {
            if (m_mapInputSources.ContainsKey(strID) == true)
            {
                return m_mapInputSources[strID];
            }
            else
            {
                return null;
            }
        }

        /// <summary>
        /// Allows you to activate or deactivate one of the sources managed by this class.
        /// </summary>
        public void activateSource(string strSourceName, bool bActive)
        {
            SourceInfo sourceInfo = m_mapInputSources[strSourceName];
            activateSource(sourceInfo.Source, sourceInfo.SourceXMLConfig, bActive);
        }

        /// <summary>
        /// Returns XML config that you can pass into initializeFromXMLConfig to
        /// set up the MTIM. This includes the list of sources, whether they are
        /// active and custom config needed by them.
        /// </summary>
        public string getXMLConfig()
        {
            string strInnerXML = "";

            // We loop through the sources and get XML config for each one...
            foreach(KeyValuePair<string, SourceInfo> pair in m_mapInputSources)
            {
                SourceInfo si = pair.Value;
                string strSourceInnerXML = "";

                // For each source we need to record:
                // a. Its assembly and class name so that we can initialize it.
                // b. Whether it is active.
                // c. Custom config for the source.

                // a.
                IMultiTouchSource source = si.Source;
                strSourceInnerXML += XMLUtility.makeElement(SOURCE_MODULE_ELEMENT, source.GetType().Module.ToString());
                strSourceInnerXML += XMLUtility.makeElement(SOURCE_CLASSNAME_ELEMENT, source.GetType().FullName);
                strSourceInnerXML += XMLUtility.makeElement(SOURCE_ACTIVE_ELEMENT, si.Active);
                strSourceInnerXML += XMLUtility.makeElement(SOURCE_CONFIG_ELEMENT, si.SourceXMLConfig);

                // We add a <source> element to the overall XML...
                strInnerXML += XMLUtility.makeElement(SOURCE_ROOT_ELEMENT, strSourceInnerXML);
            }

            // We create the full XML...
            string strXML = XMLUtility.makeElement(ROOT_ELEMENT, strInnerXML);
            return strXML;
        }

        /// <summary>
        /// Loads and initialializes the sources from the XML config passed in.
        /// </summary>
        public void initializeFromXMLConfig(string strXML)
        {
            // We find the root element for the MTIM...
            XmlDocument xmlDocument = new XmlDocument();
            xmlDocument.InnerXml = strXML;
            XmlNode rootNode = xmlDocument.SelectSingleNode(ROOT_ELEMENT);
            if (rootNode == null)
            {
                throw new Exception("XML config does not contain root element: " + ROOT_ELEMENT);
            }

            // We look for all the Source elements in the config...
            string strExceptions = "";
            XmlNodeList sourceNodes = rootNode.SelectNodes(SOURCE_ROOT_ELEMENT);
            foreach (XmlNode sourceNode in sourceNodes)
            {
                try
                {
                    initializeSourceFromXML(sourceNode);
                }
                catch (Exception ex)
                {
                    // If we catch an exception, then we have probably failed
                    // to add one of the sources. We want to note the error but
                    // continue loading other sources.
                    strExceptions += (ex.Message + Environment.NewLine);
                }
            }

            // If there were any errors we raise them to the caller...
            if (strExceptions != "")
            {
                throw new Exception(strExceptions);
            }
        }


        /// <summary>
        /// Reads XML config from the file passed in and initializes from it.
        /// </summary>
        public void initializeFromFile(string strFilename)
        {
            try
            {
                // We read the config and initialize from it...
                string strXML = File.ReadAllText(strFilename);
                initializeFromXMLConfig(strXML);
            }
            catch (Exception)
            {
                throw;
            }
            finally
            {
                // The config may have changed (if sources perform some default initialization 
                // for example). So we save the config...
                saveToFile(strFilename);
            }
        }

        /// <summary>
        /// Stores the XML config for the sources we're managing to the file passed in.
        /// </summary>
        public void saveToFile(string strFilename)
        {
            string strNewXML = getXMLConfig();
            File.WriteAllText(strFilename, strNewXML);
        }


        /// <summary>
        /// Shows a config screen that allows you to choose which sources are active and
        /// set up the sources.
        /// </summary>
        public void showConfigScreen()
        {
            MultiTouchConfig frm = new MultiTouchConfig();
            frm.ShowDialog();
        }

        #endregion

        #region Private functions

        /// <summary>
        /// Constructor. This is private as the class is a singleton.
        /// </summary>
        private MultiTouchInputManager()
        {
        }

        /// <summary>
        /// We find the config for one source from the XmlNode passed in, and
        /// create an initialize that source.
        /// </summary>
        private void initializeSourceFromXML(XmlNode sourceNode)
        {
            // We want to find the assembly name, class name, active flag and any custom config...
            string strModuleName = XMLUtility.getElementStringValue(SOURCE_MODULE_ELEMENT, sourceNode);
            string strClassName = XMLUtility.getElementStringValue(SOURCE_CLASSNAME_ELEMENT, sourceNode);
            bool bActive = XMLUtility.getElementBoolValue(SOURCE_ACTIVE_ELEMENT, sourceNode);
            string strSourceConfigXML = XMLUtility.getElementStringValue(SOURCE_CONFIG_ELEMENT, sourceNode);

            // We create the object from the module and class info...
            Assembly assembly = Assembly.LoadFrom(strModuleName);
            object oSource = assembly.CreateInstance(strClassName);

            // We add the source to the collection we're managing and activate it
            // if necessary...
            IMultiTouchSource source = (IMultiTouchSource)oSource;
            addInputSource(source, strSourceConfigXML, bActive);
            activateSource(source, strSourceConfigXML, bActive);
        }


        /// <summary>
        /// You pass in an object that implements the IMultiTouchInputSource
        /// interface and we add it to the collection of sources we manage.
        /// </summary>
        private void addInputSource(IMultiTouchSource source, string strSourceConfigXML, bool bActive)
        {
            // We check whether we're already managing this source...
            string strSourceName = source.getSourceName();
            bool bExists = m_mapInputSources.ContainsKey(strSourceName);
            if (bExists == true)
            {
                // This source is already managed...
                return;
            }

            // We add the source to the collection of sources we manage...
            SourceInfo si = new SourceInfo();
            si.Source = source;
            si.SourceXMLConfig = strSourceConfigXML;
            si.FirstPointIndex = m_iNextPointIndex;
            m_mapInputSources.Add(strSourceName, si);

            // We register to see changes to the source's config.
            source.ConfigChangedEvent += onSourceConfigChanged;

            // We note the first point index for quick lookup...
            m_mapFirstPointIndexes.Add(source, si.FirstPointIndex);

            // We find the range of point-indexes that this source needs to reserve...
            m_iNextPointIndex += source.getMaxNumberOfPoints();
        }

        /// <summary>
        /// Called when the config is changed on one of the sources we manage. We update
        /// the "master copy" of the config that we're holding.
        /// </summary>
        private void onSourceConfigChanged(object sender, ConfigChangedArgs e)
        {
            // We find the source-info for the source and update the config...
            SourceInfo sourceInfo = m_mapInputSources[e.SourceName];
            sourceInfo.SourceXMLConfig = e.XMLConfig;

            throw new NotImplementedException();
        }


        /// <summary>
        /// We activate (or deactivate) the source depending on the bActive flag passed in.
        /// </summary><remarks>
        /// Activating a source means initializing it and starting to listen to its events.
        /// Deactivating it means stopping listening to events and closing it.
        /// </remarks>
        private void activateSource(IMultiTouchSource source, string strConfigXML, bool bActive)
        {
            // Is the source already in the correct state?
            // If so we don't re-register it, as this can cause us to register
            // ourselves multiple times with the same events...
            SourceInfo sourceInfo = m_mapInputSources[source.getSourceName()];
            if (sourceInfo.Active == bActive)
            {
                return;
            }

            if (bActive == true)
            {
                source.initializeFromXMLConfig(strConfigXML);
                source.TouchDownEvent += onTouchDown;
                source.TouchUpEvent += onTouchUp;
                source.TouchMoveEvent += onTouchMove;
            }
            else
            {
                source.TouchDownEvent -= onTouchDown;
                source.TouchUpEvent -= onTouchUp;
                source.TouchMoveEvent -= onTouchMove;
                source.close();
            }

            // We update the active flag for this source.
            // Note: We do this after initializing the source so that we will not set a source
            //       as active if it has failed to initialized (and thrown an exception).
            sourceInfo.Active = bActive;
        }

        
        /// <summary>
        /// Called when one of our sources raises a touch-down event.
        /// </summary>
        private void onTouchDown(object sender, TouchArgs args)
        {
            Utility.raiseEvent(TouchDownEvent, this, convertArgs(sender, args));
        }

        /// <summary>
        /// Called when one of our sources raises a touch-up event.
        /// </summary>
        private void onTouchUp(object sender, TouchArgs args)
        {
            Utility.raiseEvent(TouchUpEvent, this, convertArgs(sender, args));
        }

        /// <summary>
        /// Called when one of our sources raises a touch-move event.
        /// </summary>
        private void onTouchMove(object sender, TouchArgs args)
        {
            Utility.raiseEvent(TouchMoveEvent, this, convertArgs(sender, args));
        }

        /// <summary>
        /// We 'translate' the point index of the args passed in. This means
        /// that we take the point index given to us by the source, and move it
        /// into the range reserved for that source.
        /// </summary>
        private TouchArgs convertArgs(object sender, TouchArgs args)
        {
            IMultiTouchSource source = (IMultiTouchSource)sender;
            int iFirstIndexForSource = m_mapFirstPointIndexes[source];
            args.PointIndex += iFirstIndexForSource;
            return args;
        }

        /// <summary>
        /// We unregister for events from the source passed in.
        /// </summary>
        private void unregisterEvents(SourceInfo si)
        {
            // We unregister from the config-changed event...
            IMultiTouchSource source = si.Source;
            source.ConfigChangedEvent -= onSourceConfigChanged;

            // If the source is not active we do not need to unregister (as we should not be
            // registered for events in the first place).
            if (si.Active == false)
            {
                return;
            }
            // We are registered for events, so we unregister...
            source.TouchDownEvent -= onTouchDown;
            source.TouchUpEvent -= onTouchUp;
            source.TouchMoveEvent -= onTouchMove;
        }

        #endregion

        #region Private data

        // Static instance memberr for the singleton...
        private static MultiTouchInputManager m_Instance = null;

        // The point index that can be assigned as the first-point-index
        // to the next source to be registered...
        private int m_iNextPointIndex = 0;

        // Map of source name to the info about the sources...
        private IDictionary<string, SourceInfo> m_mapInputSources = new Dictionary<string, SourceInfo>();
        private IDictionary<IMultiTouchSource, int> m_mapFirstPointIndexes = new Dictionary<IMultiTouchSource, int>();

        #endregion

        #region Private constants

        // Strings used by XML config...
        private string ROOT_ELEMENT = "MultiTouchInputManagerConfig";
        private string SOURCE_ROOT_ELEMENT = "Source";
        private string SOURCE_MODULE_ELEMENT = "Module";
        private string SOURCE_CLASSNAME_ELEMENT = "ClassName";
        private string SOURCE_ACTIVE_ELEMENT = "Active";
        private string SOURCE_CONFIG_ELEMENT = "Config";

        #endregion
    }
}
